#!/bin/bash

# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

. /usr/lib/crosutils/common.sh || exit 1

# Flags
DEFINE_string attach "" \
 "pgrep for the given command - 'browser' finds the chrome browser process"
DEFINE_string board "${DEFAULT_BOARD}" \
 "The board to run debugger on."
DEFINE_string remote "localhost" \
 "The IP address of the remote device."
DEFINE_integer port 1234 \
 "The port number to use for connecting to remote device."
DEFINE_integer remote_pid 0 \
 "Process ID of the running process on the remote device to which to attach."
DEFINE_string remote_file "" \
 "Full pathname of the file to be debugged on the remote device."
DEFINE_string remote_args "" \
 "Command line arguments to pass to the executable on the remote device."
DEFINE_boolean cgdb ${FLAGS_FALSE} \
 "Use cgdb curses interface rather than plain gdb."
DEFINE_boolean ssh ${FLAGS_FALSE} \
 "Use ssh stdio forwarding instead of TCP to connect to gdbserver."
DEFINE_integer ssh_port 0 \
 "The ssh port number to use for ssh connections to the remote device."

# Parse command line
FLAGS_HELP="usage: $0 [flags]"
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"
check_flags_only_and_allow_null_arg "$@" && set --

BOARD=${FLAGS_board}
BOARD_ROOT=/build/${BOARD}

if [ ${FLAGS_cgdb} -eq ${FLAGS_TRUE} ] && ! type -P cgdb >/dev/null; then
  die "Please install cgdb first"
fi

# Derive toolchain from $BOARD
CHOST=$(portageq-${BOARD} envvar CHOST)

TMP_DIR=""
SSH_PID=0
CLEAN=0
GDBINIT_FILE=~/.gdbinit

cleanup ()
{
  if [ ${CLEAN} -eq 0 ] ; then
    rm -rf ${TMP_DIR}
    if [ ${SSH_PID} -ne 0 ] ; then
      kill ${SSH_PID}
    fi
    CLEAN=1
  fi
  rm -rf ${GDBINIT_FILE}
}

# Create a temporary location in which to copy testing_rsa file; make
# sure to clean it up when we exit.

trap 'cleanup' EXIT INT TERM
TMP_DIR=$(mktemp -d)

cp ${SCRIPTS_DIR}/mod_for_test_scripts/ssh_keys/testing_rsa \
 ${TMP_DIR}/testing_rsa
chmod 0600 ${TMP_DIR}/testing_rsa

REMOTE_SSH_FLAGS=" -o StrictHostKeyChecking=no -o CheckHostIP=no\
 -o BatchMode=yes"
SSH_PORT=""
if [[ ${FLAGS_ssh_port} -ne 0 ]] ; then
  SSH_PORT=" -p ${FLAGS_ssh_port}"
fi

run_remote_command ()
{
  ssh -i ${TMP_DIR}/testing_rsa \
    ${REMOTE_SSH_FLAGS} root@${FLAGS_remote} ${SSH_PORT} \
    "$@"
}

ssh_to_remote_machine ()
{
  local command=$1
  local error_msg=$2

  if ! run_remote_command "${command}" ; then
     die "${error_msg}"
  fi
}

validate_command_options ()
{
  # Verify we have at least a board, toolchain and remote file.

  if [[ -z "${BOARD}" ]] ; then
    die "--board is required."
  fi

  if [[ -z "${CHOST}" ]] ; then
    die "Unable to determine correct toolchain from board."
  fi

  if [[ -z "${FLAGS_remote_pid}" ]] ; then
    if [[ -z "${FLAGS_remote_file}" ]] ; then
      if [[ -z "${FLAGS_attach}" ]] ; then
        die "--remote_file is required."
      fi
    fi
  fi

  # Verify that the correct cross-gdb has been built first!

  if [[ ! -f /usr/bin/${CHOST}-gdb ]] ; then
    die "${CHOST}-gdb does not exist. Please run setup_board."
  fi

  # Verify that the IP Address is currently active.

  if [[ -z "${FLAGS_remote}" ]] ; then
    die "No IP address specified."
  fi

  echo "Verifying IP address ${FLAGS_remote} (this will take a few\
 seconds)..."

  if ! ping -c 3 -q ${FLAGS_remote} > /dev/null ; then
    die "${FLAGS_remote} is not currently available."
  fi

  if [[ -n "${FLAGS_attach}" ]]; then
    if [[ "${FLAGS_attach}" == "browser" ]]; then
      FLAGS_remote_pid=$(run_remote_command \
        "pstree -p|grep session_manager|cut -d\( -f3 | cut -d\) -f1")
      if [ -z "${FLAGS_remote_pid}" ]; then
        die "Unable to find browser process"
      fi
    else
      FLAGS_remote_pid=$(run_remote_command "pgrep -f '${FLAGS_attach}'")
      local count=$(echo ${FLAGS_remote_pid} | wc -w)
      if [ ${count} -eq 0 ]; then
        die "No process matching ${FLAGS_attach}"
      elif [ ${count} -gt 1 ]; then
        error "Multiple (${count}) processes matching \"${FLAGS_attach}\":"
        local pids=$(echo "${FLAGS_remote_pid}" | tr '\n' ' ')
        run_remote_command "ps ${pids}"
        exit 1
      fi
    fi
  fi

  if [[ ${FLAGS_remote_pid} -ne 0 ]] ; then
    local ssh_cmd="readlink -e /proc/${FLAGS_remote_pid}/exe"
    local err_msg="${FLAGS_remote_pid} is not a valid PID on\
 ${FLAGS_remote}"
    FLAGS_remote_file=$(run_remote_command "${ssh_cmd}")
    if [[ $? -ne 0 ]] ; then
      die "${err_msg}"
    fi
  fi

  if [[ ! -z "${FLAGS_remote_file}" ]] ; then
    if [[ ${FLAGS_remote_file:0:1} != '/' ]] ; then
      die "--remote_file must contain full pathname."
    fi
  fi

  # Verify that the debug version of the remote file exists.

  local local_file="${BOARD_ROOT}${FLAGS_remote_file}"
  if [[ ! -x "${local_file}" ]]; then
    echo
    warn "${local_file} does not exist on your local machine or is not"
    warn "executable.  You may need to re-run build_packages before attempting to debug."
    echo
    read -p "Do you want to stop now? [y/N] " y_or_n
    case "${y_or_n}" in
      y|Y) exit 1 ;;
      *) ;;
    esac
  fi

  local local_symbols="${BOARD_ROOT}/usr/lib/debug${FLAGS_remote_file}.debug"
  local file_info="$(file "${local_file}")"
  if [[ "${file_info}" != *"not stripped"* && ! -e "${local_symbols}" ]]; then
    echo
    local package="$(qfile-${BOARD} -C -q "${FLAGS_remote_file}")"
    warn "${local_file} is stripped and ${local_symbols} does not exist"
    warn "on your local machine. The debug symbols for that package may not be"
    warn "installed."
    warn "To install the debug symbols for ${package} only, run:"
    warn " cros_install_debug_syms --board=${BOARD} ${package}"
    warn "To install the debug symbols for all available packages, run:"
    warn " cros_install_debug_syms --board=${BOARD} --all"
    echo
    read -p "Do you want to stop now? [y/N] " y_or_n
    case "${y_or_n}" in
      y|Y) exit 1 ;;
      *) ;;
    esac
  fi
}

setup_remote_iptable ()
{
  # Update the iptables on the remote device

  local ssh_cmd="/sbin/iptables -A INPUT -p tcp --dport ${FLAGS_port}\
 -j ACCEPT"
  local err_msg="Unable to add port to iptables."
  ssh_to_remote_machine "${ssh_cmd}" "${err_msg}"
}

start_remote_gdbserver ()
{
  # Start gdbserver on the remote device

  local gdbserver_cmd="gdbserver :${FLAGS_port} ${FLAGS_remote_file} ${FLAGS_remote_args}"
  if [[ ${FLAGS_remote_pid} -ne 0 ]] ; then
    gdbserver_cmd="gdbserver --attach :${FLAGS_port} ${FLAGS_remote_pid}"
  fi

  echo "Starting up gdbserver on your remote device."
  local ssh_cmd="nohup ${gdbserver_cmd} > /tmp/gdbserver.out 2>&1 &"
  local err_msg="Unable to ssh into root@${FLAGS_remote}."
  ssh_to_remote_machine "${ssh_cmd}" "${err_msg}"
}

generate_gdbinit_file ()
{
  # Create board-and-notebook-specific .gdbinit file.

  local remote_cmd='"target remote " + remote_ip_address + ":" + remote_port'
  if [ ${FLAGS_ssh} -eq ${FLAGS_TRUE} ]; then
    local gdbserver_cmd="gdbserver"
    if [[ ${FLAGS_remote_pid} -ne 0 ]] ; then
      gdbserver_cmd+=" --attach -  ${FLAGS_remote_pid}"
    else
      gdbserver_cmd+=" - ${FLAGS_remote_file} ${FLAGS_remote_args}"
    fi
    remote_cmd="\"target remote | ssh -T -i ${TMP_DIR}/testing_rsa \
      ${REMOTE_SSH_FLAGS} -o TCPKeepAlive=no -o UserKnownHostsFile=/dev/null \
      root@${FLAGS_remote} ${SSH_PORT} ${gdbserver_cmd}\""
  fi

  cat <<-EOF > ${GDBINIT_FILE}

define remote_connect
  set \$file="${FLAGS_remote_file}"
  python import os
  python filename = str (gdb.parse_and_eval ("\$file"))
  python fullname = os.path.join ("${BOARD_ROOT}", filename)
  python file_command = "file " + fullname
  python gdb.execute (file_command)
  python remote_ip_address = "${FLAGS_remote}"
  python remote_port = "${FLAGS_port}"
  python remote_cmd = ${remote_cmd}
  python gdb.execute (remote_cmd)
end

set sysroot $BOARD_ROOT
set debug-file-directory $BOARD_ROOT/usr/lib/debug
remote_connect
EOF
}


# Some special set up is required for accessing the VM on a local machine...

PORT_FORWARDING=""

if [[ "${FLAGS_remote}" == "localhost" ||
      "${FLAGS_remote}" == "127.0.0.1" ]] ; then
  if [[ -z ${SSH_PORT} ]] ; then
    SSH_PORT=" -p 9222"
  fi
  PORT_FORWARDING=" -L ${FLAGS_port}:localhost:${FLAGS_port}"
elif [ ${FLAGS_ssh} -eq ${FLAGS_FALSE} ]; then
  setup_remote_iptable
fi

validate_command_options

# If accessing the VM on the local machine, need a second ssh session open,
# for port forwarding, so gdb can find gdbserver...

SSHD_PID=0
if [[ -n "${PORT_FORWARDING}" ]] ; then
  # Call ssh directly rather than using 'ssh_to_remote_machine' because
  # too many things about this particular call are different.
  ssh -i ${TMP_DIR}/testing_rsa -N \
    ${REMOTE_SSH_FLAGS} \
    root@${FLAGS_remote} ${SSH_PORT} ${PORT_FORWARDING} &
  SSH_PID=$!
fi

if [ ${FLAGS_ssh} -eq ${FLAGS_FALSE} ]; then
  start_remote_gdbserver
  echo "gdbserver is now running remotely.  Output will be written to "
  echo "/tmp/gdbserver.out on your remote device."
fi

generate_gdbinit_file

echo "Some helpful GDB commands:"
echo "directory <path> -- causes path to be searched for source files."
echo "info functions <regexp> -- find function matching given regexp."
echo
echo "Some helpful gdb_remote specific commands:"
echo "remote_connect -- reestablish remote connection."
echo

# Start gdb on local machine.
if [ ${FLAGS_cgdb} -eq ${FLAGS_TRUE} ]; then
  cgdb -d ${CHOST}-gdb
else
  ${CHOST}-gdb
fi

cleanup
